Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / core / database / src / commonTest / kotlin / org / meshtastic / core / database / DatabaseManagerWedgedWriteTest.kt

Displaying Raw • Download

core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseManagerWedgedWriteTest.kt d5848ad5e02bd9b5b1726bedfa51ceb0faaa0240 (d5848ad5) Text, 9.81 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.database

Tff7b72import T7ee787kotlinx.coroutines.CancellationException
Tff7b72import T7ee787kotlinx.coroutines.CompletableDeferred
Tff7b72import T7ee787kotlinx.coroutines.async
Tff7b72import T7ee787kotlinx.coroutines.test.advanceTimeBy
Tff7b72import T7ee787kotlinx.coroutines.test.runCurrent
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787kotlin.test.AfterTest
Tff7b72import T7ee787kotlin.test.BeforeTest
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertIs
Tff7b72import T7ee787kotlin.test.assertNotNull
Tff7b72import T7ee787kotlin.test.assertTrue

T8b949e/**
* Covers the bounded-execution contract of [DatabaseManager.withDb] when a callback never returns — the shape a leaked
* Room 3.x connection-pool permit takes, because the pool logs and retries acquisition forever instead of throwing.
*
* One wedged callback must not stall the manager: the caller fails on its own deadline, the wedged block is abandoned
* rather than torn apart, the active pool is replaced, and later calls run on the replacement pool's fresh lane.
*/
Tff7b72class T56d364DatabaseManagerWedgedWriteTest Tb4b4b4: Te6edf3DatabaseManagerTestFixtureTb4b4b4(Tb4b4b4) Tb4b4b4{

Tf0883e@BeforeTest Tff7b72fun Td2a8ffsetUpTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3setUpFixtureTb4b4b4(Tb4b4b4)

Tf0883e@AfterTest Tff7b72fun Td2a8fftearDownTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3tearDownFixtureTb4b4b4(Tb4b4b4)

Tf0883e@Test
Tff7b72fun Td2a8ffwedgedWithDbCallbackDoesNotStallLaterCallsTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3withDbTimeoutMillisForTest Tff7b72= Te6edf3DatabaseManagerTb4b4b4.Te6edf3WITH_DB_TIMEOUT_MS
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3wedgedPool Tff7b72= Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value
Tff7b72val Te6edf3wedgeStarted Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3releaseWedge Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3wedged Tff7b72= Te6edf3async Tb4b4b4{
Te6edf3runCatching Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3withDb Tb4b4b4{
Te6edf3wedgeStartedTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3releaseWedgeTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tffa657Unit
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Te6edf3wedgeStartedTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)

Te6edf3advanceTimeByTb4b4b4(Te6edf3DatabaseManagerTb4b4b4.Te6edf3WITH_DB_TIMEOUT_MS Tff7b72+ T79c0ff1Tb4b4b4)
Tff7b72val Te6edf3failure Tff7b72= Te6edf3wedgedTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3DatabaseOperationTimeoutExceptionTff7b72>Tb4b4b4(Te6edf3failureTb4b4b4, Ta5d6ff"Ta5d6ffa wedged callback must surface as a bounded failureTa5d6ff"Tb4b4b4)

Tff7b72val Te6edf3replacement Tff7b72= Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value
Te6edf3assertTrueTb4b4b4(Te6edf3replacement Tff7b72!Tff7b72=Tff7b72= Te6edf3wedgedPoolTb4b4b4, Ta5d6ff"Ta5d6ffwedge recovery must publish a replacement poolTa5d6ff"Tb4b4b4)

T8b949e// The lane is per pool, so this call is admitted against the replacement and never queues behind the wedge.
Tff7b72var Te6edf3laterDbTb4b4b4: Te6edf3MeshtasticDatabase? Tff7b72= Tff7b72null
Tff7b72val Te6edf3later Tff7b72=
Te6edf3managerTb4b4b4.Te6edf3withDb Tb4b4b4{ Te6edf3db Tff7b72-Tff7b72>
Te6edf3laterDb Tff7b72= Te6edf3db
Te6edf3LATER_RESULT
Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(Te6edf3LATER_RESULTTb4b4b4, Te6edf3laterTb4b4b4, Ta5d6ff"Ta5d6ffa later withDb call must complete while the earlier one is still wedgedTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3laterDb Tff7b72=Tff7b72=Tff7b72= Te6edf3replacementTb4b4b4, Ta5d6ff"Ta5d6fflater calls must run on the replacement poolTa5d6ff"Tb4b4b4)

T8b949e// The abandoned block still owns its writer registration until it eventually returns.
Te6edf3assertEqualsTb4b4b4(T79c0ff1 Te6edf3to T79c0ff0Tb4b4b4, Te6edf3managerTb4b4b4.Te6edf3debugWriterCountsTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3releaseWedgeTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
T79c0ff0 Te6edf3to T79c0ff0Tb4b4b4,
Te6edf3managerTb4b4b4.Te6edf3debugWriterCountsTb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffan abandoned block must still deregister when it returnsTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffabandonedWithDbCallbackIsNeverCancelledOrReplayedTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3withDbTimeoutMillisForTest Tff7b72= Te6edf3DatabaseManagerTb4b4b4.Te6edf3WITH_DB_TIMEOUT_MS
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3wedgeStarted Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3releaseWedge Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3invocationCount Tff7b72= T79c0ff0
Tff7b72var Te6edf3observedCancellation Tff7b72= Tff7b72false
Tff7b72var Te6edf3completedNormally Tff7b72= Tff7b72false

Tff7b72val Te6edf3wedged Tff7b72= Te6edf3async Tb4b4b4{
Te6edf3runCatching Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3withDb Tb4b4b4{
Te6edf3invocationCount Tff7b72+Tff7b72= T79c0ff1
Te6edf3wedgeStartedTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Tff7b72try Tb4b4b4{
Te6edf3releaseWedgeTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3cancellationTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Te6edf3observedCancellation Tff7b72= Tff7b72true
Tff7b72throw Te6edf3cancellation
Tb4b4b4}
Te6edf3completedNormally Tff7b72= Tff7b72true
Tffa657Unit
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Te6edf3wedgeStartedTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)

Te6edf3advanceTimeByTb4b4b4(Te6edf3DatabaseManagerTb4b4b4.Te6edf3WITH_DB_TIMEOUT_MS Tff7b72+ T79c0ff1Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3wedgedTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3observedCancellationTb4b4b4, Ta5d6ff"Ta5d6ffabandoning must not tear apart a started DB-critical blockTa5d6ff"Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3completedNormallyTb4b4b4, Ta5d6ff"Ta5d6ffthe block is still suspended inside its critical sectionTa5d6ff"Tb4b4b4)

Te6edf3releaseWedgeTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3completedNormallyTb4b4b4, Ta5d6ff"Ta5d6ffthe abandoned block must be allowed to finish its critical sectionTa5d6ff"Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3observedCancellationTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3invocationCountTb4b4b4, Ta5d6ff"Ta5d6ffa started callback must never be replayed automaticallyTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0 Te6edf3to T79c0ff0Tb4b4b4, Te6edf3managerTb4b4b4.Te6edf3debugWriterCountsTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Guards against `withDb` growing a lane-scoped lock or an explicit queue: a suspended callback must not stall
* other calls on the same pool, because any callback that awaits something would then block every other write.
*
* This asserts the absence of app-level serialization only. The fixture's lane is the shared test dispatcher, not
* production's `limitedParallelism(1)` view, so lane parallelism itself is out of scope here — a single-parallelism
* dispatcher releases its slot on suspension by construction.
*/
Tf0883e@Test
Tff7b72fun Td2a8ffaSuspendedCallbackDoesNotSerializeOtherCallsOnTheSamePoolTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3withDbTimeoutMillisForTest Tff7b72= Te6edf3DatabaseManagerTb4b4b4.Te6edf3WITH_DB_TIMEOUT_MS
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3pool Tff7b72= Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value
Tff7b72val Te6edf3parkedStarted Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3releaseParked Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3parked Tff7b72= Te6edf3async Tb4b4b4{
Te6edf3runCatching Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3withDb Tb4b4b4{
Te6edf3parkedStartedTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3releaseParkedTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tffa657Unit
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Te6edf3parkedStartedTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)

Tff7b72var Te6edf3concurrentDbTb4b4b4: Te6edf3MeshtasticDatabase? Tff7b72= Tff7b72null
Tff7b72val Te6edf3concurrent Tff7b72=
Te6edf3managerTb4b4b4.Te6edf3withDb Tb4b4b4{ Te6edf3db Tff7b72-Tff7b72>
Te6edf3concurrentDb Tff7b72= Te6edf3db
Te6edf3LATER_RESULT
Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(Te6edf3LATER_RESULTTb4b4b4, Te6edf3concurrentTb4b4b4, Ta5d6ff"Ta5d6ffa concurrent call must not wait for a suspended callbackTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3concurrentDb Tff7b72=Tff7b72=Tff7b72= Te6edf3poolTb4b4b4, Ta5d6ff"Ta5d6ffboth calls are admitted against the same active poolTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value Tff7b72=Tff7b72=Tff7b72= Te6edf3poolTb4b4b4, Ta5d6ff"Ta5d6ffno recovery was needed, so the pool must not be replacedTa5d6ff"Tb4b4b4)

Te6edf3releaseParkedTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3parkedTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3isSuccessTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0 Te6edf3to T79c0ff0Tb4b4b4, Te6edf3managerTb4b4b4.Te6edf3debugWriterCountsTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Once the lifetime replacement budget is spent, the wedged pool stays published. Admitting further work against it
* would park one more block that can never finish, so under steady ingest the parked coroutines and their writer
* registrations would grow without bound. Admission must fail immediately instead.
*/
Tf0883e@Test
Tff7b72fun Td2a8ffwritesFailFastOnceWedgeRecoveryIsExhaustedTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3withDbTimeoutMillisForTest Tff7b72= Te6edf3DatabaseManagerTb4b4b4.Te6edf3WITH_DB_TIMEOUT_MS
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3releaseWedges Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3startedCallbacks Tff7b72= T79c0ff0

T8b949e// Spend the whole replacement budget, then wedge one more call against the pool recovery now refuses.
Te6edf3repeatTb4b4b4(Te6edf3MAX_WEDGE_POOL_RECOVERIES_PER_MANAGER_LIFETIME Tff7b72+ T79c0ff1Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3wedgeStarted Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3async Tb4b4b4{
Te6edf3runCatching Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3withDb Tb4b4b4{
Te6edf3startedCallbacks Tff7b72+Tff7b72= T79c0ff1
Te6edf3wedgeStartedTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3releaseWedgesTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tffa657Unit
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Te6edf3wedgeStartedTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(Te6edf3DatabaseManagerTb4b4b4.Te6edf3WITH_DB_TIMEOUT_MS Tff7b72+ T79c0ff1Tb4b4b4)
Tb4b4b4}
Tff7b72val Te6edf3wedgedPool Tff7b72= Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value
Tff7b72val Te6edf3abandonedCallbacks Tff7b72= Te6edf3startedCallbacks

Tff7b72val Te6edf3failure Tff7b72= Te6edf3runCatching Tb4b4b4{ Te6edf3managerTb4b4b4.Te6edf3withDb Tb4b4b4{ Tffa657Unit Tb4b4b4} Tb4b4b4}Tb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3DatabaseOperationTimeoutExceptionTff7b72>Tb4b4b4(Te6edf3failureTb4b4b4, Ta5d6ff"Ta5d6ffadmission must fail once recovery is exhaustedTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3abandonedCallbacksTb4b4b4, Te6edf3startedCallbacksTb4b4b4, Ta5d6ff"Ta5d6ffno further callback may be parked on the wedged poolTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3abandonedCallbacks Te6edf3to T79c0ff0Tb4b4b4,
Te6edf3managerTb4b4b4.Te6edf3debugWriterCountsTb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffa rejected call must not add a writer registrationTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value Tff7b72=Tff7b72=Tff7b72= Te6edf3wedgedPoolTb4b4b4)

T8b949e// Switching to another database publishes a healthy pool, so writes resume without a manager restart.
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LATER_RESULTTb4b4b4, Te6edf3managerTb4b4b4.Te6edf3withDb Tb4b4b4{ Te6edf3LATER_RESULT Tb4b4b4}Tb4b4b4, Ta5d6ff"Ta5d6ffa different pool must still accept writesTa5d6ff"Tb4b4b4)

Te6edf3releaseWedgesTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0 Te6edf3to T79c0ff0Tb4b4b4, Te6edf3managerTb4b4b4.Te6edf3debugWriterCountsTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72const Tff7b72val Te6edf3LATER_RESULT Tff7b72= T79c0ff4T79c0ff2
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.08s